﻿using Red.Core;
using Red.Core.Math;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace Red.Controls
{
    /// <summary>
    ///     Panel for selecting color
    /// </summary>
    sealed public partial class ColorPicker : UserControl 
    {
        #region Internal types
        
        private class PickerDot : Adorner
        {
            private const double _diameter = 4;
            private static readonly Pen _pen = new Pen( new SolidColorBrush( Color.FromRgb( 0, 0, 0 ) ), 2 );
            private Point _location;

            public Point Location
            {
                get { return _location; }
                set
                {
                    if ( value != _location )
                    {
                        _location = value;
                        InvalidateVisual();
                    }
                }
            }

            public PickerDot( Image adornedElement )
                : base( adornedElement )
            {
            }

            protected override void OnRender( DrawingContext drawingContext )
            {
                drawingContext.DrawEllipse( null, _pen, Location, _diameter, _diameter );
            }
        }

        private class SliderThumb : Adorner
        {
            private readonly Brush _brush;
            private float _position = 0f;

            public float Position
            {
                get { return _position; }
                set
                {
                    if ( value != _position )
                    {
                        _position = value;
                        InvalidateVisual();
                    }
                }
            }

            public SliderThumb( Image adornedElement )
                : base( adornedElement )
            {
                _brush = (Brush)FindResource( "LabelTextBrush" );
            }

            protected override void OnRender( DrawingContext drawingContext )
            {
                var slider = (Image)AdornedElement;
                double w = slider.ActualWidth;
                drawingContext.DrawGeometry( _brush, null,
                    GeometryHelpers.CreatePolygon( new Point( -1, _position ), new Point( -8, _position + 5 ), new Point( -8, _position - 5 ) ) );
                drawingContext.DrawGeometry( _brush, null,
                    GeometryHelpers.CreatePolygon( new Point( w + 1, _position ), new Point( w + 8, _position + 5 ), new Point( w + 8, _position - 5 ) ) );
            }
        }

        private struct PosInfo
        {
            public Point picker;
            public float slider;
        }

        private class PickerAreaInfo
        {
            public PickerDot PickerAdorner { get; private set; }

            public SliderThumb SliderAdorner { get; private set; }

            public Func<PosInfo, PosInfo> Clipper { get; private set; }

            public Func<PosInfo, Color, Color> PosToColor { get; private set; }

            public Func<Color, PosInfo> ColorToPos { get; private set; }

            public PickerAreaInfo( Image picker, Image slider, Func<PosInfo, PosInfo> clipper, Func<PosInfo, Color, Color> posToColor, Func<Color, PosInfo> colorToPos )
            {
                PickerAdorner = new PickerDot( picker );
                SliderAdorner = new SliderThumb( slider );
                Clipper = clipper;
                PosToColor = posToColor;
                ColorToPos = colorToPos;
            }
        }
        
        #endregion Internal types
        
        #region Data

        private const int _pickerSize = 256;
        private const int _sliderWidth = 32;
        private static WriteableBitmap _wheelPickerBmp;
        private static WriteableBitmap _spectrumPickerBmp;
        private readonly Dictionary< Panel, PickerAreaInfo > _pickers = new Dictionary<Panel, PickerAreaInfo>();
        private bool _internalChange = false;

        #endregion Data

        #region Properties

        public static readonly DependencyProperty ColorProperty =
            DependencyProperty.Register( "Color", typeof( Color ), typeof( ColorPicker ), new FrameworkPropertyMetadata( ColorPropertyChanged ) );

        public static readonly DependencyPropertyKey OriginalColorPropertyKey =
            DependencyProperty.RegisterReadOnly( "OriginalColor", typeof( Color ), typeof( ColorPicker ), new FrameworkPropertyMetadata() );

        public static readonly DependencyProperty OriginalColorProperty = OriginalColorPropertyKey.DependencyProperty;

        public static readonly DependencyProperty RProperty =
            DependencyProperty.Register( "R", typeof( byte ), typeof( ColorPicker ), new FrameworkPropertyMetadata( ComponentPropertyChanged ) );

        public static readonly DependencyProperty GProperty =
            DependencyProperty.Register( "G", typeof( byte ), typeof( ColorPicker ), new FrameworkPropertyMetadata( ComponentPropertyChanged ) );

        public static readonly DependencyProperty BProperty =
            DependencyProperty.Register( "B", typeof( byte ), typeof( ColorPicker ), new FrameworkPropertyMetadata( ComponentPropertyChanged ) );

        public static readonly DependencyProperty AProperty =
            DependencyProperty.Register( "A", typeof( byte ), typeof( ColorPicker ), new FrameworkPropertyMetadata( ComponentPropertyChanged ) );

        /// <summary>
        ///     Selected color
        /// </summary>
        public Color Color
        {
            get { return (Color)GetValue( ColorProperty ); }
            set { SetValue( ColorProperty, value ); }
        }

        /// <summary>
        ///     A color with which the picker was initialized
        /// </summary>
        public Color OriginalColor
        {
            get { return (Color)GetValue( OriginalColorProperty ); }
            private set { SetValue( OriginalColorPropertyKey, value ); }
        }

        /// <summary>
        ///     Red component of selected color
        /// </summary>
        public byte R
        {
            get { return (byte)GetValue( RProperty ); }
            set { SetValue( RProperty, value ); }
        }

        /// <summary>
        ///     Green component of selected color
        /// </summary>
        public byte G
        {
            get { return (byte)GetValue( GProperty ); }
            set { SetValue( GProperty, value ); }
        }

        /// <summary>
        ///     Blue component of selected color
        /// </summary>
        public byte B
        {
            get { return (byte)GetValue( BProperty ); }
            set { SetValue( BProperty, value ); }
        }

        /// <summary>
        ///     Alpha component of selected color
        /// </summary>
        public byte A
        {
            get { return (byte)GetValue( AProperty ); }
            set { SetValue( AProperty, value ); }
        }

        #endregion Properties

        #region C-tor

        public ColorPicker()
        {
            InitializeComponent();

            WriteableBitmap wheelSliderBmp = BitmapHelpers.CreateBitmap( _sliderWidth, _pickerSize );
            WriteableBitmap spectrumSliderBmp = BitmapHelpers.CreateBitmap( _sliderWidth, _pickerSize );

            if ( _wheelPickerBmp == null )
            {
                _wheelPickerBmp = BitmapHelpers.CreateBitmap( _pickerSize, _pickerSize );
                _spectrumPickerBmp = BitmapHelpers.CreateBitmap( _pickerSize, _pickerSize );
                BitmapHelpers.FillBitmap( _wheelPickerBmp, ( x, y ) => 
                    PosToColor_Wheel( new PosInfo { picker = new Point( x, y ), slider = 0f }, Color.FromRgb( 0, 0, 0 ) ) );
                BitmapHelpers.FillBitmap( _spectrumPickerBmp, ( x, y ) => 
                    PosToColor_Spectrum( new PosInfo { picker = new Point( x, y ), slider = 0f }, Color.FromRgb( 0, 0, 0 ) ) );
            }

            _wheelPicker.Source = _wheelPickerBmp;
            _wheelSlider.Source = wheelSliderBmp;
            _spectrumPicker.Source = _spectrumPickerBmp;
            _spectrumSlider.Source = spectrumSliderBmp;

            _pickers.Add( _wheel, new PickerAreaInfo( 
                _wheelPicker, _wheelSlider, Clip_Wheel, PosToColor_Wheel, ColorToPos_Wheel ) );

            _pickers.Add( _spectrum, new PickerAreaInfo(
                _spectrumPicker, _spectrumSlider, Clip_Spectrum, PosToColor_Spectrum, ColorToPos_Spectrum ) );
        }

        #endregion C-tor

        #region Private methods

        private PosInfo Clip_Wheel( PosInfo p )
        {
            const float s2 = (float)_pickerSize/2;
            float xo = (float)p.picker.X - s2;
            float yo = (float)p.picker.Y - s2;
            float r = (float)Math.Sqrt( xo * xo + yo * yo );
            if ( r >= s2 )
            {
                p.picker.X = s2 + xo / r * ( s2 - .1f );
                p.picker.Y = s2 + yo / r * ( s2 - .1f );
            }
            if ( p.slider < 0f ) p.slider = 0f;
            if ( p.slider > (float)_pickerSize - 0.01f ) p.slider = (float)_pickerSize - 0.01f; // use epsilon to prevent from "collapsing"
            return p;
        }

        private PosInfo Clip_Spectrum( PosInfo p )
        {
            if ( p.picker.X < 1 ) p.picker.X = 1;
            if ( p.picker.Y < 1 ) p.picker.Y = 1;
            if ( p.picker.X >= _pickerSize ) p.picker.X = _pickerSize - 1;
            if ( p.picker.Y >= _pickerSize ) p.picker.Y = _pickerSize - 1;
            if ( p.slider < 0 ) p.slider = 0;
            if ( p.slider > (float)_pickerSize - 0.01f ) p.slider = (float)_pickerSize - 0.01f; // use epsilon to prevent from "collapsing"
            return p;
        }

        private static Color PosToColor_Wheel( PosInfo p, Color prevColor )
        {
            float s2 = (float)_pickerSize / 2;
            float xo = (float)p.picker.X - s2;
            float yo = (float)p.picker.Y - s2;
            float r = (float)Math.Sqrt( xo * xo + yo * yo ) / s2; // [0 - 1]
            if ( r > 1f )
                return Color.FromArgb( 0, 0, 0, 0 );
            else
            {
                float a = 180f * VectorHelpers.GetAngleToOX( xo, yo ) / (float)Math.PI;
                float v = 1f - p.slider / (float)_pickerSize;
                return ColorHelpers.HsvToColor( a, r, v, prevColor.ScA );
            }
        }

        private static Color PosToColor_Spectrum( PosInfo p, Color prevColor )
        {
            float hue = 360f * ( (float)p.picker.X / _pickerSize );
            float light = 1f - (float)p.picker.Y / _pickerSize;
            float sat = 1f - p.slider / (float)_pickerSize;
            return ColorHelpers.HslToColor( hue, sat, light, prevColor.ScA );
        }

        private static PosInfo ColorToPos_Wheel( Color c )
        {
            float hue, sat, val;
            ColorHelpers.ColorToHsv( c, out hue, out sat, out val );
            const float s2 = (float)_pickerSize / 2;
            float a = (float)Math.PI * hue / 180f;
            return new PosInfo {
                picker = new Point(
                    s2 + sat * s2 * Math.Cos( a ), 
                    s2 + sat * s2 * Math.Sin( a ) 
                    ),
                slider = ( 1f - val ) * _pickerSize
            };
        }

        private static PosInfo ColorToPos_Spectrum( Color c )
        {
            float hue, sat, light;
            ColorHelpers.ColorToHsl( c, out hue, out sat, out light );
            return new PosInfo{
                picker = new Point(
                    _pickerSize * hue / 360f,
                    _pickerSize * ( 1f - light )
                    ),
                slider = ( 1f - sat ) * _pickerSize
            };
        }

        private void ReadColorFromPicker( Image picker, Point pos )
        {
            var pickerPanel = (Panel)picker.Parent;
            PickerAreaInfo pai = _pickers[pickerPanel];
            PosInfo pi = new PosInfo { picker = pos, slider = pai.SliderAdorner.Position };
            Color = pai.PosToColor( pai.Clipper( pi ), Color );
        }

        private void ReadColorFromSlider( Image slider, Point pos )
        {
            var pickerPanel = (Panel)slider.Parent;
            PickerAreaInfo pai = _pickers[pickerPanel];
            PosInfo pi = new PosInfo { picker = pai.PickerAdorner.Location, slider = (float)pos.Y };
            Color = pai.PosToColor( pai.Clipper( pi ), Color );
        }

        private void SetColorOnPicker( Panel pickerPanel, Color color )
        {
            PickerAreaInfo pai = _pickers[pickerPanel];
            PosInfo posInfo = pai.ColorToPos( color );
            pai.PickerAdorner.Location = posInfo.picker;
            pai.SliderAdorner.Position = posInfo.slider;

            var slider = (Image)pai.SliderAdorner.AdornedElement;
            BitmapHelpers.FillBitmap( (WriteableBitmap)slider.Source,
                ( x, y ) => pai.PosToColor( new PosInfo { picker = posInfo.picker, slider = (float)y }, Color.FromRgb( 255, 255, 255 ) ) );
        }

        #endregion Private methods

        #region Event handlers

        private void UserControl_Loaded( object sender, RoutedEventArgs e )
        {
            OriginalColor = Color;
        }

        private void PickerPanel_IsVisibleChanged( object sender, DependencyPropertyChangedEventArgs e )
        {
            var picker = (Panel)sender;
            if ( true.Equals( e.NewValue ) )
            { // Re-bind Adorners. It has to be done here as (apparently) an Adorner gets lost from the layer each time the control is hidden
                PickerAreaInfo pai = _pickers[picker];
                AdornerLayer.GetAdornerLayer( pai.PickerAdorner.AdornedElement ).Add( pai.PickerAdorner );
                AdornerLayer.GetAdornerLayer( pai.SliderAdorner.AdornedElement ).Add( pai.SliderAdorner );
            }
        }

        private void Picker_MouseDown( object sender, MouseButtonEventArgs e )
        {
            var picker = (Image)sender;
            ReadColorFromPicker( picker, e.GetPosition( picker ) );
            picker.CaptureMouse();
        }

        private void Picker_MouseUp( object sender, MouseButtonEventArgs e )
        {
            var picker = (Image)sender;
            picker.ReleaseMouseCapture();
        }

        private void Picker_MouseMove( object sender, MouseEventArgs e )
        {
            var picker = (Image)sender;
            if ( picker.IsMouseCaptured )
            {
                ReadColorFromPicker( picker, e.GetPosition( picker ) );
            }
        }

        private void Slider_MouseDown( object sender, MouseButtonEventArgs e )
        {
            var slider = (Image)sender;
            ReadColorFromSlider( slider, e.GetPosition( slider ) );
            slider.CaptureMouse();
        }

        private void Slider_MouseUp( object sender, MouseButtonEventArgs e )
        {
            var slider = (Image)sender;
            slider.ReleaseMouseCapture();
        }

        private void Slider_MouseMove( object sender, MouseEventArgs e )
        {
            var slider = (Image)sender;
            if ( slider.IsMouseCaptured )
            {
                ReadColorFromSlider( slider, e.GetPosition( slider ) );
            }
        }

        private static void ColorPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            ColorPicker self = (ColorPicker)d;
            Color value = (Color)e.NewValue;
            self.SetColorOnPicker( self._wheel, value );
            self.SetColorOnPicker( self._spectrum, value );
            try
            {
                self._internalChange = true;
                self.R = value.R;
                self.G = value.G;
                self.B = value.B;
                self.A = value.A;
            }
            finally
            {
                self._internalChange = false;
            }
        }

        private static void ComponentPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            var self = (ColorPicker)d;
            if ( !self._internalChange )
            {
                self.Color = Color.FromArgb( self.A, self.R, self.G, self.B );
            }
        }

        #endregion Event handlers
    }
}
